# coding: utf-8
# author: ZhenbPeng
#   date: 2017/10/17 14:17
import redis
import jsonte
import threading

from settings import Setting

JSONTE = jsonte.JsonteSerialiser(ensure_ascii=False)


class RedisClient(object):
    """
    redis client
    线程安全，支持连接池
    db0  存放临时缓存数据
    db1  存放永久存储数据
    example:
        value = RedisClient.inst0().get('key')
        value = RC.inst0().get('key')
    """
    CLIENTS = [None] * 16
    POOLS = [None] * 16
    LOCK = threading.Lock()

    @classmethod
    def inst0(cls):
        return cls._get_instance(0)

    @classmethod
    def inst1(cls):
        return cls._get_instance(1)

    @classmethod
    def _get_instance(cls, db):
        if cls.CLIENTS[db]:
            return cls.CLIENTS[db]

        with cls.LOCK:
            if cls.CLIENTS[db]:
                return cls.CLIENTS[db]

            kwargs = {
                'max_connections': 1000,
                'timeout': 120,
                'host': Setting.REDIS_HOST,
                'port': Setting.REDIS_PORT,
                'password': Setting.REDIS_PWD,
                'db': db
            }

            cls.POOLS[db] = redis.BlockingConnectionPool(**kwargs)
            del kwargs['timeout']
            del kwargs['max_connections']
            kwargs.update({'connection_pool': cls.POOLS[db]})
            cls.CLIENTS[db] = redis.Redis(**kwargs)
            return cls.CLIENTS[db]

    def __init__(self):
        raise Exception('Can\'t get RedisClient instance directly')


RC = RedisClient


def injson(choice):
    def _injson(fun):
        def __injson(*args, **kwargs):
            if args[0].__class__.JSON:
                if isinstance(choice, int):
                    args = list(args)
                    args[choice] = JSONTE.dumps(args[choice])
                    args = tuple(args)
            return fun(*args, **kwargs)

        return __injson

    return _injson


def outjson(fun):
    def _outjson(*args, **kwargs):
        data = fun(*args, **kwargs)
        if args[0].__class__.JSON and data:
            data = JSONTE.loads(data.decode("utf-8"))
        return data

    return _outjson


def expire(fun):
    def _expire(*args, **kwargs):
        data = fun(*args, **kwargs)
        args[0]._expire(kp=kwargs.get('kp', ()))
        return data

    return _expire


class RedisAccessBase(object):
    DB = 0
    KEY = 'NULL'
    TTL = 180
    JSON = False
    RCS = [RC.inst0(), RC.inst1()]
    STRING, HASH, LIST, SET, SDSET = 0, 1, 2, 3, 4
    TYPE = STRING

    def __getattr__(self, name):
        if name and len(name) > 3 and name.startswith('rc_'):
            name = name[3:]
        else:
            return super(RedisAccessBase, self).__getattr__(name)

        if hasattr(self.__class__.RCS[self.__class__.DB], name):
            fun = getattr(self.__class__.RCS[self.__class__.DB], name)
            if callable(fun):
                return fun

        raise AttributeError('\'%s\' object has no attribute %s' % (self.__class__.__name__, name))

    def _rc(self):
        return self.__class__.RCS[self.__class__.DB]

    # ========
    #   KEY
    # ========
    def _key(self, kp):
        return self.__class__.KEY % kp

    def _ttl(self):
        return self.__class__.TTL

    def _expire(self, ttl=None, kp=()):
        if ttl == None:
            ttl = self._ttl()
        if ttl == 0:
            return self._rc().persist(self._key(kp))
        return self._rc().expire(self._key(kp), ttl if ttl else self._ttl())

    def exists(self, kp=()):
        return self._rc().exists(self._key(kp))

    def delete(self, kp=()):
        return self._rc().delete(self._key(kp))

    def keys(self, pattern):
        return self._rc().keys(pattern)

    # ========
    #  STRING
    # ========
    @injson(1)
    def set(self, data, kp=()):
        if self._ttl():
            return self._rc().set(self._key(kp), data, ex=self._ttl())
        else:
            return self._rc().set(self._key(kp), data)

    @outjson
    def get(self, kp=()):
        return self._rc().get(self._key(kp))

    def mget(self, kps=()):
        return self._rc().mget([self._key(x) for x in kps])

    def incr(self, kp=()):
        return self._rc().incr(self._key(kp))

    def incrby(self, count, kp=()):
        return self._rc().incrby(self._key(kp), count)

    def decr(self, count=1, kp=()):
        return self._rc().decr(self._key(kp), count)

    # ========
    #   HASH
    # ========
    @injson(2)
    @expire
    def hset(self, subkey, data, kp=()):
        return self._rc().hset(self._key(kp), subkey, data)

    @expire
    def hincrby(self, subkey, increment, kp=()):
        return self._rc().hincrby(self._key(kp), subkey, increment)

    # @outjson
    def hgetall(self, kp=()):
        data = self._rc().hgetall(self._key(kp))
        if self.JSON:
            data = dict(map(lambda k, v: (k, JSONTE.loads(v)), data.iteritems()))
        return data

    @outjson
    def hget(self, subkey, kp=()):
        return self._rc().hget(self._key(kp), subkey)

    # @outjson
    def hmget(self, subkeys, kp=()):
        data = self._rc().hmget(self._key(kp), subkeys)
        if self.JSON:
            data = [x if not x else JSONTE.loads(x) for x in data]
        return data

    @expire
    def hmset(self, data, kp=()):
        if self.JSON:
            data = dict(map(lambda k, v: (k, JSONTE.dumps(v)), data.iteritems()))
        self._rc().hmset(self._key(kp), data)

    def hdel(self, subkey, kp=()):
        if isinstance(subkey, list) or isinstance(subkey, tuple):
            return self._rc().hdel(self._key(kp), *subkey)
        return self._rc().hdel(self._key(kp), subkey)

    # ========
    #   LIST
    # ========
    @injson(1)
    @expire
    def rpush(self, data, kp=()):
        if isinstance(data, list) or isinstance(data, tuple):
            return self._rc().rpush(self._key(kp), *data)
        return self._rc().rpush(self._key(kp), data)

    @injson(1)
    @expire
    def lpush(self, data, kp=()):
        if isinstance(data, list) or isinstance(data, tuple):
            return self._rc().lpush(self._key(kp), *data)
        return self._rc().lpush(self._key(kp), data)

    @outjson
    def lpop(self, kp=()):
        return self._rc().lpop(self._key(kp))
    @outjson
    def rpop(self, kp=()):
        return self._rc().rpop(self._key(kp))

    def lrem(self, count, value, kp=()):
        return self._rc().lrem(self._key(kp), count, value)

    # ========
    #   SET
    # ========
    @expire
    def sadd(self, data, kp=()):
        if isinstance(data, list) or isinstance(data, tuple):
            return self._rc().sadd(self._key(kp), *data)
        return self._rc().sadd(self._key(kp), data)

    def srem(self, data, kp=()):
        if isinstance(data, list) or isinstance(data, tuple):
            return self._rc().srem(self._key(kp), *data)
        return self._rc().srem(self._key(kp), data)

    def sismember(self, data, kp=()):
        return self._rc().sismember(self._key(kp), data)

    def smismember(self, data, kps=()):
        pipe = self._rc().pipeline(transaction=False)
        for kp in kps:
            pipe.sismember(self._key(kp), data)
        return pipe.execute()

    def smembers(self, kp=()):
        return self._rc().smembers(self._key(kp))

    def scard(self, kp=()):
        return self._rc().scard(self._key(kp))

    def keys(self, pattern):
        return self._rc().keys(pattern)

    def llen(self, kp=()):
        return self._rc().llen(self._key(kp))

    def lrange(self, start, end, kp=()):
        return self._rc().lrange(self._key(kp), start, end)

    def lindex(self, index, kp=()):
        return self._rc().lindex(self._key(kp), index)

    def ltrim(self, start, end, kp=()):
        return self._rc().ltrim(self._key(kp), start, end)

    def pipeline(self, transaction=True, shard_hint=None):
        return self._rc().pipeline(transaction, shard_hint)

    def execute(self, raise_on_error=True):
        return self._rc().execute(raise_on_error)


class ProxyPool(RedisAccessBase):
    DB = 1
    TTL = 0
    KEY = "proxyip"
    TYPE = RedisAccessBase.LIST
    JSON = True
    LENGTH = Setting.PROXYPOOL_LENGTH

    @property
    def length(self):
        return self.llen(kp=())


class ProxyStats(RedisAccessBase):
    DB = 1
    TTL = 0
    KEY = "proxy:stats"
    TYPE = RedisAccessBase.HASH

    def add(self, subkey, increment):
        SUBKEYS = ["ip_sum", 'ip_clean']
        if subkey not in SUBKEYS:
            raise KeyError("key %s not in redis model ProxyStats [SUBKEYS], the choice are %s" % (subkey, SUBKEYS))

        self.hincrby(subkey, increment, kp=())


class WorkerStats(RedisAccessBase):
    DB = 1
    TTL = 0
    KEY = "worker:stats:%s"
    TYPE = RedisAccessBase.HASH

    def add(self, subkey, increment, kp=()):
        SUBKEYS = ["action1", 'action2', "action3", "action99", "all_success", "page_view", "ad_click", "ad_not_load", "demo1_load"]
        if subkey not in SUBKEYS:
            raise KeyError("key %s not in redis model WorkerStats [SUBKEYS], the choice are %s" % (subkey, SUBKEYS))

        self.hincrby(subkey, increment, kp=kp)
